//
//  SocketIOClient.swift
//  QHSocketIODemo
//
//  Created by chen on 2017/5/12.
//  Copyright © 2017年 chen. All rights reserved.
//

import UIKit

public final class SocketIOClient: NSObject, SocketEngineClient, SocketParsable {
    public let socketURL: URL
    
    public private(set) var engine: SocketEngineSpec?
    public private(set) var status = SocketIOClientStatus.notConnected {
        didSet {
            switch status {
            case .connected:
                reconnecting = false
                currentReconnectAttempt = 0
            default:
                break
            }
        }
    }
    
    public var forceNew: Bool = false
    public var nsp = "/"
    public var config: SocketIOClientConfiguration
    public var reconnects = true
    public var reconnectWait = 10
    
    private let logType = "SocketIOClient"
    private let parseQueue = DispatchQueue(label: "com.socketio.parseQueue")
    
    private var anyHandler: ((SocketAnyEvent) -> Void)?
    private var currentReconnectAttempt = 0
    private var handlers = [SocketEventHandler]()
    private var reconnecting = false
    
    private let ackSemaphore = DispatchSemaphore(value: 1)
    private(set) var currentAck = -1
    private(set) var handleQueue = DispatchQueue.main
    private(set) var reconnectAttempts = -1
    
    let ackQueue = DispatchQueue(label: "com.socketio.ackQueue")
    let emitQueue = DispatchQueue(label: "com.socketio.emitQueue")
    
    var ackHandlers = NSObject()
    var waitingPackets = [SocketPacket]()
    
    public var sid: String? {
        return engine?.sid
    }
    
    public init(socketURL: URL, config: SocketIOClientConfiguration = []) {
        self.config = config
        self.socketURL = socketURL
        
        if socketURL.absoluteString.hasPrefix("https://") {
            self.config.insert(.secure(true))
        }
        
        for option in config {
            switch option {
            case let .reconnects(reconnects):
                self.reconnects = reconnects
            case let .reconnectAttempts(attempts):
                reconnectAttempts = attempts
            case let .reconnectWait(wait):
                reconnectWait = abs(wait)
            case let .nsp(nsp):
                self.nsp = nsp
            case let .log(log):
                DefaultSocketLogger.Logger.log = log
            case let .logger(logger):
                DefaultSocketLogger.Logger = logger
            case let .handleQueue(queue):
                handleQueue = queue
            case let .forceNew(force):
                forceNew = force
            default:
                continue
            }
        }
        
        self.config.insert(.path("/socket.io/"), replacing: false)
        
        super.init()
    }
    
    public convenience init(socketURL: NSURL, config: NSDictionary?) {
        self.init(socketURL: socketURL as URL, config: config?.toSocketConfiguration() ?? [])
    }
    
    deinit {
        DefaultSocketLogger.Logger.log("Client is being released", type: logType)
        engine?.disconnect(reason: "Client Deinit")
    }
    
    private func addEngine() -> SocketEngineSpec {
        DefaultSocketLogger.Logger.log("Adding engine", type: logType, args: "")
        
        return engine!
    }
    
    public func connect() {
        connect(timeoutAfter: 0, withHandler: nil)
    }
    
    public func connect(timeoutAfter: Int, withHandler handler: (() -> Void)?) {
        assert(timeoutAfter >= 0, "Invalid timeout: \(timeoutAfter)")
        
        guard status != .connected else {
            DefaultSocketLogger.Logger.log("Tried connecting on an already connected socket", type: logType)
            return
        }
        
        status = .connecting
        
        if engine == nil || forceNew {
            addEngine().connect()
        } else {
            engine?.connect()
        }
        
        guard timeoutAfter != 0 else { return }
        
        let time = DispatchTime.now() + Double(UInt64(timeoutAfter) * NSEC_PER_SEC) / Double(NSEC_PER_SEC)
        
        handleQueue.asyncAfter(deadline: time) { [weak self] in
            guard let this = self, this.status != .connected && this.status != .disconnected else { return }
            
            this.status = .disconnected
            this.engine?.disconnect(reason: "Connect timeout")
            
            handler?()
        }
    }
    
    public func nextAck() -> Int {
        ackSemaphore.wait()
        defer {
            ackSemaphore.signal()
        }
        currentAck += 1
        return currentAck
    }
    
    private func createOnAck(_ items: [Any]) -> OnAckCallback {
        return OnAckCallback(ackNumber: nextAck(), items: items, socket: self)
    }
    
    func didConnect() {
        DefaultSocketLogger.Logger.log("Socket connected", type: logType)
        status = .connected
        
        handleEvent("connect", data: [], isInternalMessage: false)
    }
    
    func didDisconnect(reason: String) {
        guard status != .disconnected else {
            return
        }
        
        DefaultSocketLogger.Logger.log("Disconnected: %@", type: logType, args: reason)
        
        reconnecting = false
        status = .disconnected
        
//        engine?.didDisconnect(reason: reason)
        handleEvent("disconnect", data: [reason], isInternalMessage: true)
        
    }
    
    public func disconnect() {
        DefaultSocketLogger.Logger.log("Closing socket", type: logType)
        
        didDisconnect(reason: "Disconnect")
    }
    
    public func emit(_ event: String, _ items: SocketData...) {
        emit(event, with: items)
    }
    
    public func emit(_ event: String, with items: [Any]) {
        guard status == .connecting else {
            handleEvent("error", data: ["Tried emitting \(event) when not connected"], isInternalMessage: true)
            return
        }
        
        _emit([event] + items)
    }
    
    public func emitWithAck(_ event: String, _ items: SocketData...) -> OnAckCallback {
        return emitWithAck(event, with: items)
    }
    
    public func emitWithAck(_ event: String, with items: [Any]) -> OnAckCallback {
        return createOnAck([event] + items)
    }
    
    func _emit(_ data: [Any], ack: Int? = nil) {
        emitQueue.async {
            guard self.status == .connected else {
                self.handleEvent("error", data: ["Tried emitting when not connected"], isInternalMessage: true)
                return
            }
        }
    }
    
    // If the server wants to know that the client received data
    func emitAck(_ ack: Int, with items: [Any]) {
        emitQueue.async {
            guard self.status == .connected else { return }
        }
    }
    
    public func engineDidError(reason: String) {
        DefaultSocketLogger.Logger.error("%@", type: logType, args: reason)
        
        handleEvent("error", data: [reason], isInternalMessage: true)
    }
    
    public func engineDidClose(reason: String) {
        parseQueue.async {
            self.waitingPackets.removeAll()
        }
        
        if status != .disconnected {
            status = .notConnected
        }
        
        if status == .disconnected || !reconnects {
            didDisconnect(reason: reason)
        } else if !reconnecting {
            reconnecting = true
            tryReconnect(reason: reason)
        }
    }
    
    public func engineDidOpen(reason: String) {
        DefaultSocketLogger.Logger.log(reason, type: "SocketEngineClient")
    }
    
    public func parseEngineMessage(_ msg: String) {
        DefaultSocketLogger.Logger.log("Should parse message: %@", type: "SocketIOClient", args: msg)
        
        parseQueue.async { self.parseSocketMessage(msg) }
    }
    
    public func parseEngineBinaryData(_ data: Data) {
        parseQueue.async { self.parseBinaryData(data) }
    }
    
    func handleAck(_ ack: Int, data: [Any]) {
        guard status == .connected else {
            return
        }
        
        DefaultSocketLogger.Logger.log("Handling ack: %@ with data: %@", type: logType, args: ack, data)
        
        handleQueue.async {
//            self.ackHandlers
        }
    }
    
    public func handleEvent(_ event: String, data: [Any], isInternalMessage: Bool, withAck ack: Int = -1) {
        guard status == .connected || isInternalMessage else {
            return
        }
        
        DefaultSocketLogger.Logger.log("Handling event: %@ with data: %@", type: logType, args: event, data)
        
        handleQueue.async {
            self.anyHandler?(SocketAnyEvent(event: event, items: data))
            
            for handler in self.handlers where handler.event == event {
                handler.executeCallback(with: data, withAck: ack, withSocket: self)
            }
        }
    }
    
    public func leaveNamespace() {
        if nsp != "/" {
            engine?.send("1\(nsp)", withData: [])
            nsp = "/"
        }
    }
    
    public func joinNamespace(_ namespace: String) {
        nsp = namespace
        
        if nsp != "/" {
            DefaultSocketLogger.Logger.log("Joining namespace", type: logType)
            engine?.send("0\(nsp)", withData: [])
        }
    }
    
    public func off(_ event: String) {
        DefaultSocketLogger.Logger.log("Removing handler for event: %@", type: logType, args: event)
        
        handlers = handlers.filter( { $0.event != event } )
    }
    
    public func off(id: UUID) {
        DefaultSocketLogger.Logger.log("Removing handler with id: %@", type: logType, args: id)
        
        handlers = handlers.filter({ $0.id != id })
    }
    
    @discardableResult
    public func on(_ event: String, callback: @escaping NormalCallback) -> UUID {
        DefaultSocketLogger.Logger.log("Adding handler for event: %@", type: logType, args: event)
        
        let handler = SocketEventHandler(event: event, id: UUID(), callback: callback)
        handlers.append(handler)
        
        return handler.id
    }
    
    /// Adds a single-use handler for an event.
    /// Returns: A unique id for the handler
    @discardableResult
    public func once(_ event: String, callback: @escaping NormalCallback) -> UUID {
        DefaultSocketLogger.Logger.log("Adding once handler for event: %@", type: logType, args: event)
        
        let id = UUID()
        
        let handler = SocketEventHandler(event: event, id: id) {[weak self] data, ack in
            guard let this = self else { return }
            this.off(id: id)
            callback(data, ack)
        }
        
        handlers.append(handler)
        
        return handler.id
    }
    
    /// Adds a handler that will be called on every event.
    public func onAny(_ handler: @escaping (SocketAnyEvent) -> Void) {
        anyHandler = handler
    }
    
    public func reconnect() {
        guard !reconnecting else {
            return
        }
        
        engine?.disconnect(reason: "manual reconnect")
    }
    
    public func removeAllHandle() {
        handlers.removeAll(keepingCapacity: false)
    }
    
    private func tryReconnect(reason: String) {
        guard reconnecting else { return }
        
        DefaultSocketLogger.Logger.log("Starting reconnect", type: logType)
        handleEvent("reconnect", data: [reason], isInternalMessage: true)
        
        _tryReconnect()
    }
    
    private func _tryReconnect() {
        guard reconnecting else { return }
        
        if reconnectAttempts != -1 && currentReconnectAttempt + 1 > reconnectAttempts || !reconnects {
            return didDisconnect(reason: "Reconnect Failed")
        }
        
        DefaultSocketLogger.Logger.log("Trying to reconnect", type: logType)
        handleEvent("reconnectAttempt", data: [(reconnectAttempts - currentReconnectAttempt)], isInternalMessage: true)
        
        currentReconnectAttempt += 1
        connect()
        
        let deadline = DispatchTime.now() + Double(Int64(UInt64(reconnectWait) * NSEC_PER_SEC)) / Double(NSEC_PER_SEC)
        
        DispatchQueue.main.asyncAfter(deadline: deadline, execute: _tryReconnect)
    }
    
    var testHandlers: [SocketEventHandler] {
        return handlers
    }
    
    func setTestable() {
        status = .connected
    }
    
    func setTestEngine(_ engine: SocketEngineSpec?) {
        self.engine = engine
    }
    
    func emitTest(event: String, _ data: Any...) {
        _emit([event] + data)
    }
}
